import logging
import os
import requests
from requests.adapters import HTTPAdapter
import configparser
import time
import datetime
from multiprocessing.pool import ThreadPool
from multiprocessing import cpu_count
import json
import subprocess


global configer,logger

class Config(object):
    def __init__(self):
        file='pydownbili.ini'
        self.configer = configparser.ConfigParser()
        self.configer.read(file,encoding="utf-8")

        self.sslverify=True
        s = self.getString("bili", 'sslverify')
        self.sslverify = s == "true" or s == "True"
        self.poolthreadcount=int(self.getString('bili','poolthreadcount'))



    def getString(self,section,name):
        return self.configer.get(section,name,raw=True)

    def getBoolean(self,section,name):
        return self.configer.getboolean(section,name,raw=True)
    def getFloat(self,section,name):
        return self.configer.getfloat(section,name,raw=True)


class PartDownloader(object):
    def __init__(self,url,partsize,threadcount,outfile):
        self.url = url
        self.partsize=partsize
        self.threadcount=threadcount
        self.pool = ThreadPool(configer.poolthreadcount)
        self.outfile=outfile
        self.totalsize = 0
        self.finished=[]

    #
    def execdownload(self):
        session = SessionBuilder().buildSession(configer, section='bili')
        logger.info("begin {}".format(self.url))
        try:
            connecttimeout = configer.getFloat('bili', 'connecttimeout')
            readtimeout = configer.getFloat('bili', 'readtimeout')

            resp = session.head(self.url,timeout=(connecttimeout, readtimeout), verify=configer.sslverify)
            if resp.status_code==200:
                if "Content-Length" in resp.headers:
                    self.totalsize=int(resp.headers['Content-Length'])
                    totalsizeg = self.totalsize/1024/1024/1024
                    totalsizeg = '{:.2f}'.format(totalsizeg)

                    logger.info(f'totalsize={self.totalsize}({totalsizeg}GB),url={self.url}')
                    self.splittask()
                    return

            resp = session.get(self.url, timeout=(connecttimeout, readtimeout), verify=configer.sslverify)
            if resp.status_code != 200:
                logger.error('404 {}'.format(self.url))
                return None
            return resp.content

        except Exception as e:
            logger.error(e)
            return None
        return
    def splittask(self):
        taskcount = int(self.totalsize / self.partsize)
        if self.totalsize % self.partsize >0:
            taskcount+=1
        logger.info(f'下载任务{taskcount}个')
        self.tasklist=[]

        startindex=0
        total=0
        taskid=0
        for i in range(taskcount):
            task={}
            self.tasklist.append(task)
            task['taskid']='{:0>5d}'.format(taskid)
            taskid+=1
            task['startindex']=startindex
            thissize = min(self.totalsize-total, self.partsize)
            task['endindex']=startindex+thissize-1
            total+=thissize
            startindex+=thissize

        self.finished=[]
        while(len(self.finished)!=len(self.tasklist)):
            self.pool.map(self.exectask,self.tasklist)
            logger.info(f'taskcount={len(self.tasklist)},finishedcount={len(self.finished)}')
        #合成
        with open(self.outfile,'wb') as f:
            for t in self.tasklist:
                with open(t['outfile'],'rb') as partf:
                    c = partf.read()
                    f.write(c)

        for t in self.tasklist:
            os.remove(t['outfile'])
        logger.info(f'已下载{self.outfile}')


    def exectask(self,task):
        if task in self.finished:
            return
        fn = os.path.split(self.outfile)[-1]+'.'+task['taskid']
        tempdir = configer.getString('bili', 'tempdir')
        outfile=os.path.join(tempdir,fn)
        task['outfile']=outfile

        if os.path.exists(outfile):
            self.finished.append(task)
            return

        session = SessionBuilder().buildSession(configer, section='bili')

        si = task['startindex']
        ei = task['endindex']
        session.headers['Range']=f'bytes={si}-{ei}'
        connecttimeout = configer.getFloat('bili', 'connecttimeout')
        readtimeout = configer.getFloat('bili', 'readtimeout')
        try:
            resp = session.get(self.url, timeout=(connecttimeout, readtimeout), verify=configer.sslverify)
        except Exception as ee:
            logger.error(ee)
            return
        if resp.status_code!=206:
            raise Exception(f'{self.url}不支持断点续传')
        with open(outfile,'wb') as f:
            f.write(resp.content)

        self.finished.append(task)
        logger.info(f'已临时文件下载{outfile}')


class SessionBuilder(object):
    def __init__(self):
        pass

    def buildSession(self, configer,section='head'):
        cookie=configer.getString(section,'cookie')
        cookiedict = self.str2dict(cookie)
        session = requests.Session()
        session.cookies =requests.utils.cookiejar_from_dict(cookiedict)
        headers={}
        headers['Accept']=configer.getString(section,'accept')
        headers['Accept-Encoding']=configer.getString(section,'accept-encoding')
        headers['Accept-Language']=configer.getString(section,'accept-language')
        headers['Connection']=configer.getString(section,'connection')
        headers['User-Agent']=configer.getString(section,'user-agent')
        headers['Referer']='https://www.bilibili.com/video/'
        session.headers = headers
        session.mount('http://', HTTPAdapter(max_retries=3))
        session.mount('https://', HTTPAdapter(max_retries=3))
        return session

    def buildPayload(self, configer):
        params = configer.getString('params', 'params')
        ws = params.split('&')
        paramdict={}
        for s in ws:
            nvs=s.split('=')
            paramdict[nvs[0].strip()] = nvs[1].strip()
        return paramdict


    def str2dict(self,cookie):
        if cookie is None or len(cookie.strip())==0:return {}
        ws = cookie.split(';')
        cookiedict={}
        for s in ws:
            nv = s.split('=')
            cookiedict[nv[0].strip()] = nv[1].strip()
        return cookiedict

class MSFormatter(logging.Formatter):
    def formatTime(self, record, datefmt=None):
        ct = self.converter(record.created)
        if datefmt:
            s = time.strftime(datefmt, ct)
        else:
            # t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
            # s = "%s,%03d" % (t, record.msecs)
            s = str(datetime.datetime.now())
        return s

class Logger(object):
    def __init__(self):
        self.logger = logging.getLogger("suning")
        self.logger.setLevel(logging.INFO)
        formatter = MSFormatter("%(asctime)s %(threadName)s  %(message)s", datefmt=None)
        sh = logging.StreamHandler()
        sh.setFormatter(formatter)
        self.logger.addHandler(sh)
        if os.path.exists('logs')==False:
            os.mkdir("logs")
        fh = logging.FileHandler('logs/pydownbili.log',encoding="utf-8")
        fh.setFormatter(formatter)
        self.logger.addHandler(fh)

    def info(self,msg):
        self.logger.log(logging.INFO,msg)
    def error(self,msg):
        self.logger.log(logging.ERROR,msg)


class Videoinfo(object):
    def __init__(self):
        self.bvid=""
        self.avid=""
        self.cid=""
        self.title=""

def getBvid(url):
    baseurl=r'https://www.bilibili.com/video/'
    p = url.find(baseurl)
    if p < 0:
         return None
    l = len(baseurl)
    bvid=url[l:l+12]
    return bvid

def webquery(url):
    session = SessionBuilder().buildSession(configer, section='bili')
    logger.info("begin {}".format(url))
    try:
        connecttimeout=configer.getFloat('bili','connecttimeout')
        readtimeout=configer.getFloat('bili','readtimeout')

        resp = session.get(url, timeout=(connecttimeout,readtimeout), verify=configer.sslverify)
        if resp.status_code != 200:
            logger.error('404 {}'.format(url))
            return None
        return resp.content

    except Exception as e:
        logger.error(e)
        return None



def queryBVinfo(bvid):
    url = r'https://api.bilibili.com/x/web-interface/view/detail?bvid={}'.format(bvid)
    v = json.loads(webquery(url))
    data = v['data']
    view=data['View']

    videoinfo=Videoinfo()
    videoinfo.bvid=view['bvid']
    videoinfo.cid=str(view['cid'])
    videoinfo.title=view['title']

    return videoinfo




def buildPlayurl(videoinfo):
    params={}
    params['cid']=videoinfo.cid
    params['bvid']=videoinfo.bvid

    params['from_client']='BROWSER'
    params['fourk']='1'
    params['fnver']='0'
    params['fnval']='4048'
    params['qn']='125'

    ps=''
    for key in params:
        v=params[key]
        if len(ps)>0 : ps= ps +'&'
        ps = ps + key+'='+v

    url = r'https://api.bilibili.com/x/player/wbi/playurl?'+ps
    return url

def buildBangumiurl(videoinfo):
    quality=125
    baseUrl = r'https://api.bilibili.com/pgc/player/web/playurl?cid={}&qn={}&fourk=1&fnver=0&fnval=4048'.format(videoinfo.cid,quality)
    return baseUrl



class VAInfo(object):
    def __init__(self):
        self.videoinfos=[]
        self.audioinfos=[]

class PlayVideoInfo(object):
    def __init__(self):
        self.baseurl=''
        self.id=0

class PlayAudioInfo(object):
    def __init__(self):
        self.baseurl=''
        self.id=0

def buildPlaylist(playurls):
    vainfo = VAInfo()
    videos = playurls['data']['dash']['video']
    for v in videos:
        playvideoinfo=PlayVideoInfo()
        vainfo.videoinfos.append(playvideoinfo)
        playvideoinfo.id = v['id']
        playvideoinfo.baseurl=v['baseUrl']
        playvideoinfo.width=v['width']
        playvideoinfo.height=v['height']
        playvideoinfo.frameRate=v['frameRate']
        playvideoinfo.SegmentBase=v['SegmentBase']

    audios = playurls['data']['dash']['audio']
    for a in audios:
        audioinfo=PlayAudioInfo()
        vainfo.audioinfos.append(audioinfo)
        audioinfo.id = a['id']
        audioinfo.baseurl=a['baseUrl']
        audioinfo.frameRate=v['frameRate']
        audioinfo.SegmentBase=v['SegmentBase']


    return vainfo

class Downloadtask(object):
    def __init__(self):
        self.baseurl=''
        self.outfile=None


def execDownload(task):
    if os.path.exists(task.outfile):
        return

    logger.info('开始下载' + task.outfile)
    partsize=1024*1024*10
    partdownloader = PartDownloader(task.baseurl,partsize,configer.poolthreadcount,task.outfile)
    partdownloader.execdownload()

    # data = webquery(task.baseurl)
    # if data is None or len(data) == 0:
    #     return
    # with open(task.outfile, 'wb') as f:
    #     f.write(data)
    #     logger.info('已写入' + task.outfile+",大小"+str(len(data)))

#在html中查找json
def searchJson(content,startstr):
    p = content.find(startstr)
    if p<0: return
    p += len(startstr)
    p1 = p

    ct = 0
    l = len(content)
    while(p1<l):
        if content[p1]=='{':
           ct+=1;
        elif content[p1]=='}':
           ct -=1
        p1 += 1
        if ct==0 :
            break
    s = content[p:p1]
    obj = json.loads(s)
    return obj

def parseEpisodes(content):
    obj = searchJson(content, r'{"props"')
    title= obj['props']['pageProps']['dehydratedState']['queries'][0]['state']['data']['seasonInfo']['mediaInfo']['title']
    logger.info('title={}'.format(title))
    episodes = obj['props']['pageProps']['dehydratedState']['queries'][0]['state']['data']['seasonInfo']['mediaInfo']['episodes']

    list=[]
    #n个
    for episode in episodes:
        videoinfo = Videoinfo()
        videoinfo.avid=episode['aid']
        videoinfo.bvid=episode['bvid']
        videoinfo.cid=episode['cid']
        videoinfo.title=episode['long_title']
        list.append(videoinfo)
    return list

def mergeMp4(vainfo):
    # 合成音频视频
    tempdir=configer.getString('bili','tempdir')
    if vainfo.pageindex is None:
        videofile=os.path.join(tempdir,'{}_v.ts'.format(vainfo.title))
    else:
        videofile = os.path.join(tempdir, '{:0>2d}_{}_v.ts'.format(vainfo.pageindex,vainfo.title))
    audiofile=None
    if len(vainfo.audioinfos)>0:
        if vainfo.pageindex is None:
            audiofile = os.path.join(tempdir,'{}_a.ts'.format(vainfo.title))
        else:
            audiofile = os.path.join(tempdir,'{:0>2d}_{}_a.ts'.format(vainfo.pageindex,vainfo.title))

    exepath = configer.getString('bili', 'ffmpeg')
    if vainfo.pageindex is None:
        outpath=r'{}\{}.mp4'.format(vainfo.tsdir,vainfo.title)
    else:
        outpath=r'{}\{:0>2d}_{}.mp4'.format(vainfo.tsdir,vainfo.pageindex,vainfo.title)

    logger.info(r'开始合成视频:{}'.format(outpath))
    if audiofile is not None:
        cmd = r'{} -i {} -i {}  -vcodec copy  ' \
                  '-acodec copy -y {}'.format(exepath,videofile, audiofile, outpath)
        proc=subprocess.Popen(cmd.split(), stdout=subprocess.PIPE,encoding='utf-8')
    else:
        cmd = r'{} -i {}  -vcodec copy ' \
              r'-acodec copy   -y {}'.format(exepath, videofile, outpath)
        proc=subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
    logger.info(r'已合成视频:{}'.format(outpath))

def execVAinfotask(vainfo):
    task = Downloadtask()
    task.baseurl = vainfo.videoinfos[0].baseurl
    if vainfo.pageindex is None:
        videofile = '{}_v.ts'.format(vainfo.title)
    else:
        videofile = '{:0>2d}_{}_v.ts'.format(vainfo.pageindex,vainfo.title)
    tempdir = configer.getString('bili', 'tempdir')
    task.outfile = os.path.join(tempdir, videofile)
    #下载视频
    execDownload(task)

    #下载音频
    if len(vainfo.audioinfos)>0:
        task = Downloadtask()
        task.baseurl = vainfo.audioinfos[0].baseurl
        if vainfo.pageindex is None:
            audiofile = '{}_a.ts'.format(vainfo.title)
        else:
            audiofile = '{:0>2d}_{}_a.ts'.format(vainfo.pageindex,vainfo.title)
        task.outfile = os.path.join(tempdir, audiofile)
        execDownload(task)

    #合并视频、音频
    mergeMp4(vainfo)



def parsedownloadvideo(url,tsdir):
    content = webquery(url)
    content = str(content,encoding='utf-8')
    #obj=searchJson(content, r'{"aid"')
    vinfo=searchJson(content,r'"videoData":')
    #vinfo = obj['videoData']

    videoinfo=Videoinfo()
    title=vinfo['title']
    title=filtefilename(title)
    videoinfo.bvid=vinfo['bvid']
    videoinfo.cid=str(vinfo['cid'])
    videoinfo.title=title
    if 'aid' in vinfo:
        videoinfo.avid=vinfo['aid']
    if 'pages' in vinfo:
        pages=vinfo['pages']

    if pages is None:
        # 生成播放链接
        playurl = buildPlayurl(videoinfo)
        logger.info('playurl={}'.format(playurl))
        playurls = json.loads(webquery(playurl))
        vainfo = buildPlaylist(playurls)
        vainfo.tsdir = tsdir
        vainfo.title=title
        execVAinfotask(vainfo)
    else:
        index=0
        startindex = configer.getFloat('bili', 'episodestart')
        endindex = configer.getFloat('bili', 'episodeend')
        for page in pages:
            index=index+1
            if index>=startindex and index<=endindex:
                videoinfo.cid = str(page['cid'])
                playurl =  buildPlayurl(videoinfo)
                logger.info('playurl={}'.format(playurl))
                playurls = json.loads(webquery(playurl))
                vainfo = buildPlaylist(playurls)
                vainfo.tsdir = tsdir
                vainfo.title=filtefilename(page['part'])
                vainfo.pageindex=index
                execVAinfotask(vainfo)



#生成下载视频、音频url
def buildplayurlbangumi(videoinfo):
    url=r'https://api.bilibili.com/pgc/player/web/v2/playurl?support_multi_audio=true&avid={}&cid={}&qn=125&fnver=0&fnval=4048&fourk=1&gaia_source=&from_client=BROWSER'.format(videoinfo.avid,videoinfo.cid)
    content = webquery(url)
    content = str(content,encoding='utf-8')
    obj = json.loads(content)
    video_info=obj['result']['video_info']
    if video_info['is_preview'] == 1:
        vainfo = VAInfo()
        playvideoinfo=PlayVideoInfo()
        vainfo.videoinfos.append(playvideoinfo)
        playvideoinfo.baseurl=video_info['durl'][0]['url']
        vainfo.previewflag=True
        return vainfo

    videos = obj['result']['video_info']['dash']['video']

    vainfo = VAInfo()
    for v in videos:
        playvideoinfo=PlayVideoInfo()
        vainfo.videoinfos.append(playvideoinfo)
        playvideoinfo.id = v['id']
        playvideoinfo.baseurl=v['baseUrl']
        playvideoinfo.width=v['width']
        playvideoinfo.height=v['height']
        playvideoinfo.frameRate=v['frameRate']
        playvideoinfo.SegmentBase=v['SegmentBase']
        vainfo.previewflag=False

    audios = obj['result']['video_info']['dash']['audio']
    for a in audios:
        audioinfo=PlayAudioInfo()
        vainfo.audioinfos.append(audioinfo)
        audioinfo.id = a['id']
        audioinfo.baseurl=a['baseUrl']
        audioinfo.frameRate=v['frameRate']
        audioinfo.SegmentBase=v['SegmentBase']
    return vainfo

#生成cheese下载视频、音频url
def buildplayurlcheese(videoinfo):
    url=f'https://api.bilibili.com/pugv/player/web/playurl?avid={videoinfo.avid}&cid={videoinfo.cid}&qn=80&fnver=0&fnval=16&fourk=1&gaia_source=&from_client=BROWSER&ep_id={videoinfo.ep_id}&session=ed16a3895608f32a17e6f50cfbef6f6b&drm_tech_type=2'
    content = webquery(url)
    content = str(content,encoding='utf-8')
    obj = json.loads(content)
    if obj['code'] < 0:
        raise Exception(obj['message'])
    video_info=obj['data']
    if video_info['is_preview'] == 1:
        vainfo = VAInfo()
        playvideoinfo=PlayVideoInfo()
        vainfo.videoinfos.append(playvideoinfo)
        playvideoinfo.baseurl=video_info['durl'][0]['url']
        vainfo.previewflag=True
        return vainfo

    videos = obj['data']['dash']['video']

    vainfo = VAInfo()
    for v in videos:
        playvideoinfo=PlayVideoInfo()
        vainfo.videoinfos.append(playvideoinfo)
        playvideoinfo.id = v['id']
        playvideoinfo.baseurl=v['base_url']
        playvideoinfo.width=v['width']
        playvideoinfo.height=v['height']
        vainfo.previewflag=False

    audios = obj['data']['dash']['audio']
    for a in audios:
        audioinfo=PlayAudioInfo()
        vainfo.audioinfos.append(audioinfo)
        audioinfo.id = a['id']
        audioinfo.baseurl=a['base_url']
        audioinfo.frameRate=v['frame_rate']
    return vainfo


def filtefilename(title):
    title=title.replace(r'?', '')
    title=title.replace(r' ', '')
    title=title.replace(r'*', '')
    title=title.replace(r'|', '')
    #title=title.replace(r'⚡', '')
    #title=title.replace(r'❷', '')
    #title=title.replace(r'❾', '')

    return title

def parsedownloadbangumi(url,tsdir):
    content = webquery(url)
    content = str(content, encoding='utf-8')
    videolist = parseEpisodes(content)

    #开始集、结束集
    startindex = configer.getFloat('bili','episodestart')
    endindex = configer.getFloat('bili','episodeend')

    fileindex=1
    for videoinfo in videolist:
        if fileindex<startindex:
            fileindex+=1
            continue

        if fileindex>endindex:
            break
        vainfo = buildplayurlbangumi(videoinfo)
        if vainfo is None:
            continue
        title = videoinfo.title
        if vainfo.previewflag:
            title='预览_'+title
        vainfo.title = filtefilename(title)
        vainfo.title = '{:0>2}'.format(fileindex)+'_'+vainfo.title
        fileindex+=1
        vainfo.tsdir = tsdir
        execVAinfotask(vainfo)

def parseCheeseepisodes(content):
    objs = json.loads(content)['data']

    videolist=[]
    for vinfo in objs['episodes']:
        videoinfo = Videoinfo()
        videoinfo.avid=vinfo['aid']
        videoinfo.cid=vinfo['cid']
        videoinfo.title=vinfo['title']
        videoinfo.ep_id=vinfo['id']
        videolist.append(videoinfo)
    return videolist


def parsedownloadcheese(url,tsdir):
    tag='https://www.bilibili.com/cheese/play/ep'
    p=len(tag)
    p1 = url.find('?',p)
    if p1<0:p1=len(url)
    epid=url[p:p1]

    sessionurl=f'https://api.bilibili.com/pugv/view/web/season?ep_id={epid}'
    content = webquery(sessionurl)
    content = str(content, encoding='utf-8')
    videolist = parseCheeseepisodes(content)
    if videolist is None:
        logger.error('下载失败')
        return

    #开始集、结束集
    startindex = configer.getFloat('bili','episodestart')
    endindex = configer.getFloat('bili','episodeend')

    fileindex=1
    for videoinfo in videolist:
        if fileindex<startindex:
            fileindex+=1
            continue

        if fileindex>endindex:
            break
        vainfo = buildplayurlcheese(videoinfo)
        if vainfo is None:
            continue
        title = videoinfo.title
        if vainfo.previewflag:
            title='预览_'+title
        vainfo.title = filtefilename(title)
        vainfo.title = '{:0>2}'.format(fileindex)+'_'+vainfo.title
        fileindex+=1
        vainfo.tsdir = tsdir
        execVAinfotask(vainfo)


def mainentry():
    global configer, logger
    configer = Config()
    pool = ThreadPool(configer.poolthreadcount)
    logger=Logger()
    logger.info('cpucount={},poolthread={}'.format(cpu_count(),configer.poolthreadcount))

    tsdir = configer.getString('bili','outdir')
    if os.path.exists(tsdir)==False:
        os.mkdir(tsdir)

    tempdir=configer.getString('bili','tempdir')
    if os.path.exists(tempdir)==False:
        os.mkdir(tempdir)

    url=configer.getString('bili', 'url')

    #下载视频
    if url.startswith('https://www.bilibili.com/video/BV'):
        parsedownloadvideo(url,tsdir)
    elif url.startswith('https://www.bilibili.com/bangumi/play/ss'):
    #下载番剧
        parsedownloadbangumi(url, tsdir)
    elif url.startswith('https://www.bilibili.com/bangumi/play/ep'):
    # 下载番剧
        parsedownloadbangumi(url, tsdir)
    elif url.startswith('https://www.bilibili.com/cheese/play/ep'):
    # 下载课程
        parsedownloadcheese(url, tsdir)
    else:
        logger.error(f'不能识别{url}')


if __name__ == '__main__':
    mainentry()